package work.chenxr.aop.aspect;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONWriter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Level;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;
import work.chenxr.aop.annotations.OperateLog;
import work.chenxr.aop.dto.OperateLogModel;
import work.chenxr.aop.service.OperateLogService;
import work.chenxr.aop.utils.HttpContextUtils;
import work.chenxr.aop.utils.IpUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.IntStream;

/**
 * @Author Chenxr
 * @Date 2023/09/18
 * @Description 操作日志，切面处理类
 */
@Slf4j
@Aspect
@Component
public class OperateLogAspect {
    private final OperateLogService operateLogService;

    @Value("${operate-log.print}")
    private boolean print;

    public OperateLogAspect(OperateLogService operateLogService) {
        this.operateLogService = operateLogService;
    }

    private static boolean ignoreArgs(Object object) {
        Class<?> clazz = object.getClass();

        // 处理数组
        if (clazz.isArray()) {
            return IntStream.range(0, Array.getLength(object)).anyMatch(index -> ignoreArgs(Array.get(object, index)));
        }

        // 处理集合
        if (Collection.class.isAssignableFrom(clazz)) {
            return ((Collection<?>) object).stream().anyMatch((Predicate<Object>) OperateLogAspect::ignoreArgs);
        }

        // 处理Map
        if (Map.class.isAssignableFrom(clazz)) {
            return ignoreArgs(((Map<?, ?>) object).values());
        }

        return object instanceof MultipartFile
                || object instanceof HttpServletRequest
                || object instanceof HttpServletResponse
                || object instanceof BindingResult;
    }

    private static <T extends Annotation> T getMethodAnnotation(ProceedingJoinPoint joinPoint, Class<T> annotationClass) {
        return ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(annotationClass);
    }

    private static <T extends Annotation> T getClassAnnotation(ProceedingJoinPoint joinPoint, Class<T> annotationClass) {
        return ((MethodSignature) joinPoint.getSignature()).getMethod().getDeclaringClass().getAnnotation(annotationClass);
    }

    @Around("@annotation(operateLog)")
    public Object around(ProceedingJoinPoint joinPoint, OperateLog operateLog) throws Throwable {
        // 记录开始时间
        LocalDateTime startTime = LocalDateTime.now();
        try {
            //执行方法
            Object result = joinPoint.proceed();

            //保存日志
            this.saveLog(joinPoint, operateLog, startTime, 1);

            return result;
        } catch (Exception e) {
            //保存日志
            this.saveLog(joinPoint, operateLog, startTime, 0, e);
            throw e;
        }
    }

    private void saveLog(ProceedingJoinPoint joinPoint, OperateLog operateLog, LocalDateTime startTime, Integer status, Exception... e) {
        OperateLogModel operateLogModel = new OperateLogModel();

        // 执行时长
        long duration = Duration.between(startTime, LocalDateTime.now()).toMillis();
        operateLogModel.setDuration((int) duration);

        //TODO 用户信息未定义，可接入安全框架后获取，例如：SecurityUser.getUserId()
        //UserDetail user = SecurityUser.getUser();
        //if (user != null) {
        //    operateLogModel.setUserId(user.getId());
        //    operateLogModel.setOperator(user.getRealName());
        //}

        // 操作类型
        operateLogModel.setOperateType(operateLog.type()[0].getValue());
        // 设置module值
        operateLogModel.setModule(operateLog.module());
        // 设置name值
        operateLogModel.setName(operateLog.name());

        //TODO 以下参数需引入接口文档，引入方法参考 chenxr-spring-boot3-knife4j

        // 如果没有指定module值，则从tag读取
        //if (StringUtils.isBlank(operateLogModel.getModule())) {
        //    io.swagger.v3.oas.annotations.tags.Tag tag = this.getClassAnnotation(joinPoint,  io.swagger.v3.oas.annotations.tags.Tag.class);
        //    if (tag != null) {
        //        operateLogModel.setModule(tag.name());
        //    }
        //}

        // 如果没有指定name值，则从operation读取
        //if (StringUtils.isBlank(operateLogModel.getName())) {
        //    io.swagger.v3.oas.annotations.Operation operation = this.getMethodAnnotation(joinPoint, io.swagger.v3.oas.annotations.Operation.class);
        //    if (operation != null) {
        //        operateLogModel.setName(operation.summary());
        //    }
        //}

        // 请求相关
        HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
        if (request != null) {
            operateLogModel.setIp(IpUtils.getIpAddr(request));
            operateLogModel.setAddress(IpUtils.getAddressByIP(operateLogModel.getIp()));
            operateLogModel.setUserAgent(request.getHeader(HttpHeaders.USER_AGENT));
            operateLogModel.setReqUri(request.getRequestURI());
            operateLogModel.setReqMethod(request.getMethod());
        }

        // 异常相关
        if (ArrayUtils.isNotEmpty(e)) {
            StringBuilder excBuilder = new StringBuilder().append(e.getClass().getName());
            for (StackTraceElement stackTraceElement : e[0].getStackTrace()) {
                // 堆栈异常信息优化，只保存本系统业务异常信息
                if (!stackTraceElement.getClassName().startsWith("work.chenxr")) {
                    break;
                }
                excBuilder.append("\n\t").append(stackTraceElement);
            }
            operateLogModel.setLogLevel(Level.ERROR.name());
            operateLogModel.setException(excBuilder.toString());
        }

        operateLogModel.setReqParams(this.obtainMethodArgs(joinPoint));
        operateLogModel.setStatus(status);

        // 保存操作日志
        operateLogService.saveLog(operateLogModel);
    }

    private String obtainMethodArgs(ProceedingJoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        String[] argNames = methodSignature.getParameterNames();
        Object[] argValues = joinPoint.getArgs();

        // 拼接参数
        Map<String, Object> args = new HashMap<>(argValues.length);
        for (int i = 0; i < argNames.length; i++) {
            String argName = argNames[i];
            Object argValue = argValues[i];
            args.put(argName, ignoreArgs(argValue) ? "[ignore]" : argValue);
        }

        return JSON.toJSONString(args);
    }

    @AfterReturning(value = "@annotation(work.chenxr.aop.annotations.OperateLog)", returning = "res")
    public void afterReturning(JoinPoint joinPoint, Object res) {
        if (this.print) {
            log.info("****************************************请求内容****************************************");
            HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
            assert request != null;
            String contentType = request.getHeader("content-type");
            log.info("Path: {}, {}{}",
                    request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + request.getRequestURI(),
                    request.getMethod(), StringUtils.isBlank(contentType) ? StringUtils.EMPTY : ", " + contentType);
            log.info("Auth: {}", request.getHeader("Authorization"));
            log.info("Body: {}", this.obtainMethodArgs((ProceedingJoinPoint) joinPoint));
            log.info("Resp: {}", ObjectUtils.isEmpty(res) ? StringUtils.EMPTY : JSON.toJSONString(res, JSONWriter.Feature.WriteMapNullValue));
        }
    }
}
